Skip to content

feat(m365): add DMARC domain check#11840

Open
0xTaoZ wants to merge 1 commit into
prowler-cloud:masterfrom
0xTaoZ:codex/defender-domain-dmarc-records
Open

feat(m365): add DMARC domain check#11840
0xTaoZ wants to merge 1 commit into
prowler-cloud:masterfrom
0xTaoZ:codex/defender-domain-dmarc-records

Conversation

@0xTaoZ

@0xTaoZ 0xTaoZ commented Jul 5, 2026

Copy link
Copy Markdown

Summary

  • add defender_domain_dmarc_records_published for Microsoft 365 Defender
  • evaluate each Defender DKIM domain for a _dmarc.<domain> TXT record
  • pass only when the DMARC policy is enforcing (p=quarantine or p=reject)
  • add unit coverage for reject, quarantine, p=none, missing DNS, and empty domain config cases

Closes #11800

Validation

  • uv run pytest tests/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published_test.py -q
  • uv run black --check --target-version py310 prowler/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published.py tests/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published_test.py
  • uv run flake8 prowler/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published.py tests/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published_test.py

Risk

Low. The check is additive and only performs DNS TXT lookups for domains already present in the Defender DKIM configuration list. DNS lookup failures are treated as non-enforcing DMARC findings instead of raising.

@0xTaoZ 0xTaoZ requested a review from a team as a code owner July 5, 2026 00:15
@github-actions github-actions Bot added provider/m365 Issues/PRs related with the M365 provider metadata-review community Opened by the Community labels Jul 5, 2026
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

No Conflicts

No conflict markers, and the branch merges cleanly into its base.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new Microsoft 365 Defender check, defender_domain_dmarc_records_published, which verifies each Exchange Online domain publishes an enforcing DMARC TXT record via DNS lookup, along with check metadata and unit tests covering pass/fail/edge-case scenarios.

Changes

DMARC enforcement check

Layer / File(s) Summary
Metadata and module scaffolding
prowler/providers/m365/services/defender/defender_domain_dmarc_records_published/__init__.py, .../defender_domain_dmarc_records_published.metadata.json
Adds an empty package init file and a metadata JSON defining check identity, DMARC enforcement expectations, risk, remediation, references, and medium severity.
DMARC policy lookup and check execution
.../defender_domain_dmarc_records_published.py
Implements execute() iterating DKIM configuration domains, performing DNS TXT lookups for _dmarc.<domain>, extracting the p= policy via regex, and marking PASS for quarantine/reject and FAIL otherwise; includes helper functions for DNS query and TXT record string normalization.
Unit tests and mocking helpers
tests/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published_test.py
Adds mocking helpers for the Defender client, M365 provider, and Defender initialization, and tests for reject, quarantine (split chunks), none, missing record, and empty domain-list scenarios.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Check as defender_domain_dmarc_records_published
  participant DefenderClient as Defender client
  participant DNS as DNS resolver

  Check->>DefenderClient: read dkim_configurations
  loop for each domain
    Check->>DNS: query TXT _dmarc.<domain>
    DNS-->>Check: TXT records or NXDOMAIN
    Check->>Check: extract p= policy via regex
    Check->>Check: set PASS if quarantine/reject else FAIL
  end
Loading

Suggested reviewers: danibarranqueroo, pedrooot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation matches #11800 by checking _dmarc TXT records per domain and passing only on p=quarantine or p=reject.
Out of Scope Changes check ✅ Passed No unrelated changes are evident; the new metadata, check code, package init, and tests all support the requested DMARC check.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title is concise and clearly summarizes the main change: adding a DMARC domain check for M365 Defender.
Description check ✅ Passed The description covers the summary, linked issue, validation, and risk, so it is mostly complete despite missing some template sections.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@prowler/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published.py`:
- Around line 1-7: The module-level imports in
defender_domain_dmarc_records_published are missing an explicit dependency on
dns.exception, even though dns.exception.DNSException is referenced later in the
check logic. Update the imports near the top of
defender_domain_dmarc_records_published.py to explicitly import dns.exception
alongside dns.resolver, so the exception handling in the check remains clear and
robust.
- Around line 62-66: The _get_dmarc_policy helper currently calls
dns.resolver.resolve without a bounded DNS wait, so add explicit timeout
handling there. Update the resolve call for _dmarc.{domain} TXT lookups to pass
a finite lifetime/timeout and keep the existing DNSException fallback so slow or
unresponsive domains do not stall the scan.

In
`@tests/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published_test.py`:
- Around line 139-153: Rename the empty-result test in
defender_domain_dmarc_records_published_test.py from test_no_dkim_configurations
to test_no_resources so it matches the standard naming convention for checks;
keep the existing test body and assertion in test_no_resources, and leave the
PASS/FAIL coverage tests unchanged. Use the current test method definition as
the only symbol to update.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f1d31ac1-e60e-465a-b61c-84322fd444a3

📥 Commits

Reviewing files that changed from the base of the PR and between efb86bb and f765cc5.

📒 Files selected for processing (4)
  • prowler/providers/m365/services/defender/defender_domain_dmarc_records_published/__init__.py
  • prowler/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published.metadata.json
  • prowler/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published.py
  • tests/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published_test.py

Comment on lines +1 to +7
import re
from typing import List

import dns.resolver

from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.defender import defender_client

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

dns.exception is used but never explicitly imported.

Only dns.resolver is imported; dns.exception.DNSException (line 65) works only because dns.resolver transitively imports dns.exception and registers it as an attribute of the dns package. This is fragile — an explicit import documents the dependency and protects against future dnspython internal changes.

🧹 Proposed fix
 import re
 from typing import List

+import dns.exception
 import dns.resolver

Also applies to: 65-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@prowler/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published.py`
around lines 1 - 7, The module-level imports in
defender_domain_dmarc_records_published are missing an explicit dependency on
dns.exception, even though dns.exception.DNSException is referenced later in the
check logic. Update the imports near the top of
defender_domain_dmarc_records_published.py to explicitly import dns.exception
alongside dns.resolver, so the exception handling in the check remains clear and
robust.

Comment on lines +62 to +66
def _get_dmarc_policy(domain: str) -> str:
try:
txt_records = dns.resolver.resolve(f"_dmarc.{domain}", "TXT")
except dns.exception.DNSException:
return ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant Defender DNS lookup checks
rg -n "dns\.resolver\.resolve|Resolver\(|lifetime=|timeout=" prowler/providers/m365/services/defender -g '*.py'

# Inspect the target file with line numbers
cat -n prowler/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published.py

# Inspect any sibling SPF/DKIM checks if present
fd -a -t f '.*(spf|dkim|dmarc).*\.py$' prowler/providers/m365/services/defender

Repository: prowler-cloud/prowler

Length of output: 3967


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.py

# Look for any explicit dnspython timeout patterns elsewhere in the repo
rg -n "dns\.resolver\.resolve\(|lifetime=|timeout=" prowler -g '*.py'

Repository: prowler-cloud/prowler

Length of output: 7492


Add a DNS timeout to dns.resolver.resolve. This call currently relies on dnspython’s default lifetime, so a few slow or unresponsive domains can stall the scan; pass a bounded lifetime/timeout here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@prowler/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published.py`
around lines 62 - 66, The _get_dmarc_policy helper currently calls
dns.resolver.resolve without a bounded DNS wait, so add explicit timeout
handling there. Update the resolve call for _dmarc.{domain} TXT lookups to pass
a finite lifetime/timeout and keep the existing DNSException fallback so slow or
unresponsive domains do not stall the scan.

Comment on lines +139 to +153
def test_no_dkim_configurations(self):
defender_client = _mock_defender_client()

with (
_mock_m365_provider(),
_mock_defender_init(),
mock.patch(
f"{CHECK_MODULE}.defender_client.defender_client",
new=defender_client,
),
):
check = _get_check_class()()
result = check.execute()

assert len(result) == 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename test_no_dkim_configurations to test_no_resources.

As per path instructions, "Every check needs test_no_resources (len==0), pass (PASS) and fail (FAIL)." This test covers the correct scenario (empty result set) but uses a non-standard name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/providers/m365/services/defender/defender_domain_dmarc_records_published/defender_domain_dmarc_records_published_test.py`
around lines 139 - 153, Rename the empty-result test in
defender_domain_dmarc_records_published_test.py from test_no_dkim_configurations
to test_no_resources so it matches the standard naming convention for checks;
keep the existing test body and assertion in test_no_resources, and leave the
PASS/FAIL coverage tests unchanged. Use the current test method definition as
the only symbol to update.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Opened by the Community metadata-review new-check provider/m365 Issues/PRs related with the M365 provider

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[New Check]: Ensure DMARC records are published for all Exchange Online domains

2 participants